/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.framework.xml;

import axil.etc.Identity;
import axil.etc.NameValue;
import axil.framework.error.InternalException;
import axil.stdlib.collection.type.Range;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import static axil.framework.Functions.*;


/**
 * A simple, code-based mechanism for supporting flexible XML deserialization.
 * If you have ever had to suffer through the nonsense that is JAXB, you can see
 * why something simple and elegant makes sense.
 */
public class InputXML implements Identity {
	private String reference;
	private Node node;


	InputXML(String reference, Node node) {
		assert node != null;
		this.reference = reference;
		this.node = node;
	}


	public static InputXML read(String reference, Reader in) {
		Document doc;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.parse(new InputSource(in));
		} catch (SAXException e) 					{
			throw abort(e);
		} catch (IOException e)					{
			throw abort(e);
		} catch (ParserConfigurationException e)	{
			throw abort(e);
		} finally {
			close(in);
		}
		return new InputXML(reference, doc.getDocumentElement());
	}


	public static InputXML read(String reference, InputStream in) {
		Document doc;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.parse(in);
		} catch (SAXException e) 					{
			throw abort(e);
		} catch (IOException e)					{
			throw abort(e);
		} catch (ParserConfigurationException e)	{
			throw abort(e);
		} finally {
			close(in);
		}
		return new InputXML(reference, doc.getDocumentElement());
	}


	//#########################################################################
	// RAW TAG PROCESSING
	//#########################################################################

	/**
	 * Get the unique identity for this object. The string returned is never
	 * empty and contains no punctuation characters beyond a possible '-'. The
	 * value is suitable for use as an identifier.
	 */
	public String identity() {
		return node.getNodeName();
	}


	public InputXML find(String tag) {
		NodeList list = node.getChildNodes();
		final int size = list.getLength();
		for (int i = 0; i < size; i++) {
			Node n = list.item(i);
			if (n.getNodeName().equals(tag)) {
				return new InputXML(reference, n);
			}
		}
		return null;
	}


	private Node tag(String tag, boolean required) {
		NodeList list = node.getChildNodes();
		final int size = list.getLength();
		for (int i = 0; i < size; i++) {
			Node n = list.item(i);
			if (n.getNodeName().equals(tag)) {
				return n;
			}
		}
		if (required) {
			throw problem("Missing required tag.", nv("tag", tag));
		}
		return null;
	}


	public InputXML element(String tag) {
		InputXML e = find(tag);
		if (e == null) {
			throw problem("Missing required tag.", nv("tag", tag));
		}
		return e;
	}


	public Iterator elements(String tag) {
		return new XMLIterator(reference, node.getChildNodes(), tag);
	}


	//#########################################################################
	// VALUES AS XML TAGS
	//#########################################################################

	/**
	 * Get the root object from an XML document. This does not specify the tag
	 * since the tag is defined in the root object itself via the @XML
	 * annotation. Any object that can be a root must provide an @XML annotation
	 * with the name.
	 */
	public <T> T root(Class<T> type) {
		XMLFactory factory = XMLFactory.factory(type);
		// return (T)factory.construct(element(factory.tag()));
		return (T)factory.construct(this);
	}


	/**
	 * Get a single object of the given type identified by the given tag. The
	 * object is required -- an exception is thrown if the object cannot be
	 * found.
	 */
	public <T> T object(Class<T> type, String tag) {
		return (T) XMLFactory.factory(type).construct(element(tag));
	}


	/**
	 * Get a single object of the given type identified by the given tag. If
	 * the object cannot be found, then the default value is returned.
	 */
	public <T> T object(Class<T> type, String tag, T defval) {
		InputXML xml = find(tag);
		if (xml == null) {
			return defval;
		}
		return (T) XMLFactory.factory(type).construct(xml);
	}


	/**
	 * Get a set of objects of the given type identified by the given XML tag
	 * name. If no objects are found, then an empty list is returned.
	 */
	public <T> List<T> objects(Class<T> type, String tag) {
		return objects(type, tag, Range.any);
	}


	/**
	 * Get a set of objects of the given type identified by the given XML tag
	 * name. The range given specifies the minimum number that must be found.
	 */
	public <T> List<T> objects(Class<T> type, String tag, Range many) {
		XMLFactory factory = XMLFactory.factory(type);
		ArrayList objects = new ArrayList();
		NodeList children = node.getChildNodes();
		final int size = children.getLength();
		for (int i = 0; i < size; i++) {
			Node n = children.item(i);
			if (n.getNodeName().equals(tag)) {
				objects.add(factory.construct(new InputXML(reference, n)));
			}
		}
		if (! many.bounds(objects)) {
			throw problem("Minimum number of objects not provided.",
			              nv("type", type),
			              nv("tag", tagify(tag)),
			              nv("expecting", many),
			              nv("actual", objects.size()));
		}
		return objects;
	}


	/**
	 * Get a set of objects of the given type identified by the given XML tag
	 * name. The enclosing tag is the wrapper tag surrounding the values. The
	 * range given specifies the minimum number that must be found.
	 */
	public <T> List<T> objects(Class<T> type, String enclosing, String tag,
	                           Range many) {
		InputXML xml = find(enclosing);
		if (xml == null) {
			if (many.some()) {
				throw problem(
				    "At least one of this object is required. The enclosing " +
				    "tag for the required list was not found.",
				    nv("type", type),
				    nv("tag", tagify(enclosing)),
				    nv("expecting", many));
			}
			return new ArrayList<T>();
		}
		return xml.objects(type, tag, many);
	}


	/**
	 * Get a single abstract type contained within the given XML tag. The object
	 * is required -- failure to find a matching object will throw an exception.
	 */
	public <T> T variant(Class<T> type, String tag) {
		return element(tag).variant(type);
	}


	/**
	 * Get a single abstract type contained within this XML tag. The object is
	 * required -- failure to find a matching object will throw an exception.
	 */
	public <T> T variant(Class<T> type) {
		XMLFactory factory = XMLFactory.factory(type);
		NodeList children = node.getChildNodes();
		final int size = children.getLength();
		for (int i = 0; i < size; i++) {
			Node n = children.item(i);
			if (factory.matches(n.getNodeName())) {
				return (T)factory.construct(new InputXML(reference, n));
			}
		}
		throw problem("An object of that type cannot be found in this XML.",
		              nv("type", type));
	}


	/**
	 * Get a collection of the given abstract type contained within this XML
	 * tag. If no objects are found, then an exception is thrown.
	 */
	public <T> List<T> variants(Class<T> type) {
		return variants(type, Range.some);
	}


	/**
	 * Get a collection of the given abstract type contained within this XML
	 * tag. The range given specifies the minimum number that must be found.
	 */
	public <T> List<T> variants(Class<T> type, Range many) {
		XMLFactory factory = XMLFactory.factory(type);
		ArrayList objects = new ArrayList();
		NodeList children = node.getChildNodes();
		final int size = children.getLength();
		for (int i = 0; i < size; i++) {
			Node n = children.item(i);
			if (factory.matches(n.getNodeName())) {
				objects.add(factory.construct(new InputXML(reference, n)));
			}
		}
		if (! many.bounds(objects)) {
			throw problem("Minimum number of objects not provided.",
			              nv("type", type),
			              nv("expecting", many),
			              nv("actual", objects.size()));
		}
		return objects;
	}


	/**
	 * Get a collection of the given abstract type contained within this XML
	 * tag. The range given specifies the minimum number that must be found.
	 */
	public <T> List<T> variants(Class<T> type, String tag, Range many) {
		InputXML xml = find(tag);
		if (xml == null) {
			if (many.some()) {
				throw problem(
				    "The required XML container tag could not be found.",
				    nv("tag", tag));
			}
			return Collections.emptyList();
		}
		return variants(type, many);
	}


	public String content(String tag) {
		return data(tag, true).trim();
	}


	public String content(String tag, String defval) {
		String v = data(tag, false);
		if (v == null) {
			return defval;
		}
		v = v.trim();
		return empty(v) ? defval : v;
	}


	public String content() {
		return data().trim();
	}


	public List<String> strings(String enclosing, String tag) {
		return strings(enclosing, tag, Range.some);
	}


	public List<String> strings(String enclosing, String tag, Range range) {

		ArrayList<String> values = new ArrayList<String>();
		Node parent = tag(enclosing, range.some());
		if (parent != null) {
			NodeList children = parent.getChildNodes();
			final int size = children.getLength();
			for (int i = 0; i < size; i++) {
				Node n = children.item(i);
				if (n.getNodeName().equals(tag)) {
					values.add(contents(n));
				}
			}
			if (! range.bounds(values)) {
				throw problem(
				    "At least the minimum number of values must be specified.",
				    nv("minimum", range.min()));
			}
		}
		return values;
	}


	//#########################################################################
	// VALUES AS XML ATTRIBUTES
	//#########################################################################

	public int integer(String attribute) {
		try {
			return Integer.parseInt(attribute(attribute));
		} catch (NumberFormatException e) {
			throw problem("The value of this attribute must be an integer.",
			              nv("attribute", attribute));
		}
	}


	public int integer(String attribute, int defval) {
		String v = attribute(attribute, null);
		if (v == null) {
			return defval;
		}
		try {
			return Integer.parseInt(attribute(attribute));
		} catch (NumberFormatException e) {
			throw problem("The value of this attribute must be an integer.",
			              nv("attribute", attribute));
		}
	}


	public boolean bool(String tag) {
		return Boolean.parseBoolean(attribute(tag));
	}


	public boolean bool(String attribute, boolean defval) {
		String v = attribute(attribute, null);
		return v == null ? defval : Boolean.parseBoolean(v);
	}


	public String text(String attribute) {
		return attribute(attribute);
	}


	public String text(String attribute, String defval) {
		String v = attribute(attribute, null);
		return empty(v) ? defval : v;
	}


	private <T extends Enum<T>> T find(String attribute, String value, T[] values) {
		for (T e : values) {
			if (e.name().equals(value)) {
				return e;
			}
		}
		throw problem(
		    "The value is not one of the known values for the enumerated type.",
		    nv("attribute", attribute), nv("value", value));
	}


	public <T extends Enum<T>> T ordinal(String attribute, T[] values) {
		return find(attribute, attribute(attribute), values);
	}


	public <T extends Enum<T>> T ordinal(String attribute, T[] values, T defval) {
		String v = attribute(attribute, null);
		return v == null ? defval : find(attribute, v, values);
	}


	/**
	 * Get an object from a text representation embedded in an attribute. The
	 * object must provide a static method for creation of objects from string
	 * with this signature:
	 *
	 * 		public static [T] from(String text)
	 *
	 * If no attribute with that given name can be found, then the default
	 * value is returned.
	 */
	public <T> T embedded(String attribute, Class<T> type, T defval) {
		String v = attribute(attribute, null);
		if (v == null) {
			return defval;
		}
		return (T)invoke(null, method(type, "from", String.class), v);
	}


	/**
	 * Get an object from a text representation embedded in an attribute. The
	 * object must provide a static method for creation of objects from string
	 * with this signature:
	 *
	 * 		public static [T] from(String text)
	 *
	 * If no attribute with that given name can be found, then an exception is
	 * thrown.
	 */
	public <T> T embedded(String attribute, Class<T> type) {
		String v = attribute(attribute);
		return (T)invoke(null, method(type, "from", String.class), v);
	}


	//#########################################################################
	// SUPPORT LOGIC
	//#########################################################################

	private String attribute(String name) {
		Node n = node.getAttributes().getNamedItem(name);
		if (n == null) {
			throw problem("Missing required XML attribute.",
			              nv("attribute", name));
		}
		return contents(n);
	}


	private String attribute(String name, String defval) {
		Node n = node.getAttributes().getNamedItem(name);
		if (n == null) {
			return defval;
		}
		return contents(n);
	}


	private String data(String tag, boolean required) {
		NodeList list = node.getChildNodes();
		final int size = list.getLength();
		for (int i = 0; i < size; i++) {
			Node n = list.item(i);
			if (n.getNodeName().equals(tag)) {
				return contents(n);
			}
		}
		if (required) {
			throw problem("Missing required XML tag.", nv("tag", tag));
		}
		return null;
	}


	private String data() {
		return contents(node);
	}


	private String contents(Node anode) {
		return normalize(anode.getTextContent());
	}


	private String tagify(String tag) {
		return '<' + tag + '>';
	}


	private InternalException problem(String message, NameValue... values) {
		StringBuilder b = new StringBuilder();
		b.append("There is a problem with the XML at or near the following:\n");
		if (! empty(reference)) {
			b.append(reference);
			b.append('\n');
		}
		int indent = 1;
		for (String s : walk(node, new ArrayList<String>())) {
			b.append(blanks(indent * 4));
			b.append(s);
			b.append('\n');
			indent ++;
		}
		b.append(message);
		return new InternalException(b.toString(), values);
	}


	private List<String> walk(Node node, List<String> path) {
		Node parent = node.getParentNode();
		if (parent != null) {
			walk(parent, path);
		}
		path.add(summary(node));
		return path;
	}


	private String summary(Node node) {
		StringBuilder b = new StringBuilder();
		b.append('<');
		b.append(node.getNodeName());
		NamedNodeMap attrs = node.getAttributes();
		if (attrs != null) {
			for (int i = 0; i < 3; i++) {
				Node attr = attrs.item(i);
				if (attr != null) {
					b.append(' ');
					b.append(attr.getNodeName());
					b.append('=');
					b.append(stringify(summarize(contents(attr))));
				}
			}
		}
		b.append('>');
		return b.toString();
	}


	/**
	 * Return a string representation of this object. This provides a summary
	 * of what the XML looks like where an error has occurred. The string
	 * returned is never null.
	 */
	public String toString() {
		return summary(node);
	}
}

